package com.guavamail.smtp;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.mina.core.buffer.BufferDataException;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.ssl.SslFilter;
import org.apache.mina.filter.ssl.SslFilter.SslFilterMessage;
import org.apache.mina.transport.socket.SocketSessionConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.guavamail.smtp.exception.CommandException;
import com.guavamail.smtp.auth.AuthenticationHandler;
import com.guavamail.smtp.command.CommandHandler;

public class SMTPConnectionHandler extends IoHandlerAdapter {
	private static Logger log = LoggerFactory.getLogger(SMTPConnectionHandler.class);

	public final static String CONTEXT_ATTRIBUTE = SMTPConnectionHandler.class.getName() + ".ctx";
	/**
	 * A thread safe variable that represents the number of active connections.
	 */
	private AtomicInteger numberOfConnections = new AtomicInteger(0);
	private SMTPServerConfig config;
	private CommandHandler commandHandler;
	private AuthenticationHandler authenticationHandler;

	public SMTPConnectionHandler(SMTPServerConfig config, CommandHandler handler,
			AuthenticationHandler authenticationHandler) {
		this.config = config;
		this.commandHandler = handler;
		this.authenticationHandler = authenticationHandler;
	}

	@Override
	public void sessionCreated(IoSession session) throws Exception {
		updateNumberOfConnections(+1);

		if (session.getTransportMetadata().getSessionConfigType() == SocketSessionConfig.class) {
			((SocketSessionConfig) session.getConfig()).setReceiveBufferSize(128);
			((SocketSessionConfig) session.getConfig()).setSendBufferSize(64);
		}

		session.getConfig().setIdleTime(IdleStatus.READER_IDLE, config.getConnectionTimeout() / 1000);

		// We're going to use SSL negotiation notification.
		session.setAttribute(SslFilter.USE_NOTIFICATION);

		// Init protocol internals
		log.debug("SMTP connection count: {}", getNumberOfConnections());

		SMTPContext minaCtx = new SMTPContext(config, authenticationHandler);
		session.setAttribute(CONTEXT_ATTRIBUTE, minaCtx);

		try {
			if (hasTooManyConnections()) {
				log.debug("Too many connections to the SMTP server !");
				sendResponse(session, "554 Transaction failed. Too many connections.");
			} else {
				sendResponse(session, "220 " + config.getHostName() + " ESMTP " + config.getSystemName());
			}
		} catch (IOException e) {
			try {
				sendResponse(session, "450 Problem when connecting. Please try again later.");
			} catch (IOException e1) {
			}

			log.debug("Error on session creation", e);

			session.close(false);
		}
	}

	@Override
	public void sessionClosed(IoSession session) throws Exception {
		updateNumberOfConnections(-1);
	}

	@Override
	public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
		log.debug("Exception occured :", cause);
		boolean fatal = true;

		try {
			if (cause instanceof BufferDataException) {
				sendResponse(session, "501 " + cause.getMessage());
			} else if (cause instanceof CommandException) {
				fatal = false;
				sendResponse(session, "500 Syntax error");
			} else {
				sendResponse(session, "450 Problem attempting to execute commands. Please try again later.");
			}
		} catch (IOException e) {
		} finally {
			if (fatal)
				session.close(false);
		}
	}

	@Override
	public void messageReceived(IoSession session, Object message) throws Exception {
		if (message == null) {
			if (log.isDebugEnabled())
				log.debug("no more lines from client");
			return;
		}

		if (message instanceof SslFilterMessage) {
			if (log.isDebugEnabled())
				log.debug("SSL FILTER message -> " + message);
			return;
		}

		SMTPContext minaCtx = (SMTPContext) session.getAttribute(CONTEXT_ATTRIBUTE);
		if (message instanceof InputStream) {
			minaCtx.setInputStream((InputStream) message);
			this.commandHandler.handleCommand("DATA", session, minaCtx);
		} else {
			String line = (String) message;

			if (log.isDebugEnabled())
				log.debug("C: " + line);

			this.commandHandler.handleCommand(line, session, minaCtx);
		}

	}

	@Override
	public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
		try {
			sendResponse(session, "421 Timeout waiting for data from client.");
		} catch (IOException ioex) {
		} finally {
			session.close(false);
		}
	}

	/**
	 * Update the number of active connections.
	 * 
	 * @param delta
	 */
	private void updateNumberOfConnections(int delta) {
		int count = numberOfConnections.addAndGet(delta);
		log.debug("Active connections count = {}", count);
	}

	/**
	 * @return The number of open connections
	 */
	public int getNumberOfConnections() {
		return numberOfConnections.get();
	}

	/**
	 * @return Are we over the maximum amount of connections ?
	 */
	private boolean hasTooManyConnections() {
		return (config.getMaxConnections() > -1 && getNumberOfConnections() >= config.getMaxConnections());
	}

	/**
	 * 
	 * @param session
	 * @param response
	 * @throws IOException
	 */
	public static void sendResponse(IoSession session, String response) throws IOException {
		if (log.isDebugEnabled())
			log.debug("S: " + response);

		if (response != null)
			session.write(response);

		SMTPContext minaCtx = (SMTPContext) session.getAttribute(CONTEXT_ATTRIBUTE);
		if (!minaCtx.getSessionStatus().isActive())
			session.close(false);
	}
}
